package ssi.peeno;

import java.io.*;
import java.net.Socket;
import java.security.NoSuchAlgorithmException;
import java.util.Date;

import javax.crypto.SecretKey;

import org.bouncycastle.util.encoders.Base64;
import org.json.me.JSONException;
import org.json.me.JSONObject;

import ssi.peeno.ciphers.AsymCipher;
import ssi.peeno.ciphers.SymCipher;
import ssi.peeno.managers.ConfigManager;
import ssi.peeno.managers.KeyManager;
import ssi.peeno.managers.MessageManager;
import ssi.peeno.managers.SQLManager;
import ssi.peeno.managers.UserManager;
import ssi.peeno.messages.highlevel.*;
import ssi.peeno.messages.lowlevel.*;
import ssi.peeno.model.User;
import ssi.peeno.util.Javabean2JSON;

public class PeenoWorker extends Thread {
	private Socket socket;
	private PrintWriter output;
	private BufferedReader input;
	private RequestParser parser;
	private MessageManager msgManager;
	private KeyManager keyManager;
	private UserManager usrManager;
	private SQLManager sqlManager;
	private long tempInteger;
	private User currentUser;
	private SymCipher sessionCipher;
	boolean logged = true;
	/**
	 * Model behaviour as a state machine driven by message type
	 * 
	 * 0 initial state: nor registration nor comunication(why i'm awake?)
	 * 1 REG : registration start
	 * 2 REG/ACK : not more used
	 * 3 REG/ACK/R : end registration/mutual authentication
	 * 4 SYN : 
	 * 5 SYN/ACK
	 * 6 ACK
	 * 7 MSG
	 * 
	 */
	private int state = 0;
	private SecretKey sessionKey;
	private int sessionId;
	private long timeStamp;
	private boolean userAuthenticated;
	private int msgCount;
	public boolean friendsDownloadForced;
	
	public PeenoWorker(Socket sck) {
		this.socket = sck;
	}
	
	public KeyManager getKeyManager() {
		return keyManager;
	}

	/**
	 * Main thread function, listen on socket for arriving of message, decrypt the message and call the function 
	 * handler of the message
	 */
	@Override
	public void run() {
		ConfigManager.log("Thread partito");
		initWorker();
		String inputLine = null;
		int lineLen;
		char[] cryptedMsg;
		String msg;
		try {
			while ((inputLine = input.readLine()) != null && logged) {
				if (inputLine.equals("REG")) {
						state = 1;
						ConfigManager.log("REG RECEIVED");
						String lenLine = input.readLine();
						lineLen = Integer.parseInt(lenLine);
						cryptedMsg = new char[lineLen];
						
						int bytecount = input.read(cryptedMsg,0, lineLen);
						ConfigManager.log(bytecount + "==" + lineLen);
						while (bytecount<lineLen){
							ConfigManager.log(bytecount + "==" + lineLen);
							bytecount +=input.read(cryptedMsg,bytecount, lineLen-bytecount);
						}
						msg = PeenoServer.getCipher().decryptWithPrivate(new String(cryptedMsg));
						ConfigManager.log(msg);
						onRegReceived(msg);
				} else if (inputLine.equals("REG/ACK/R")) {
					state = 3;
					ConfigManager.log("REG/ACK/R RECEIVED");
					registrationComplete();
				} else if (inputLine.equals("SYN")) {
						state = 4;
						ConfigManager.log("SYN RECEIVED");
						lineLen = Integer.parseInt(input.readLine());
						cryptedMsg = new char[lineLen];
						input.read(cryptedMsg, 0, lineLen);
						msg = PeenoServer.getCipher().decryptWithPrivate(new String(cryptedMsg));
						ConfigManager.log(msg);
						authSynReceived(msg);
				} else if (inputLine.equals("SYN/ACK")) {
					ConfigManager.log("SYN/ACK RECEIVED");
					int ssnid = Integer.parseInt(input.readLine());
					lineLen = Integer.parseInt(input.readLine());
					cryptedMsg = new char[lineLen];
					input.read(cryptedMsg, 0, lineLen);
					//TODO check if sessionCipher has correct session id, else get session id and key from db.
					if (sessionCipher.getSessionId()!=ssnid) {
						String key = usrManager.loadSessionKey(ssnid);
						sessionCipher.setKey(key);
						sessionCipher.setSessionId(ssnid);
					}
					msg = sessionCipher.decrypt(new String(cryptedMsg));
					ConfigManager.log(msg);
					onSynAckReceived(msg);
				} else if (inputLine.equals("REC")){
					ConfigManager.log("REC RECEIVED");
					lineLen = Integer.parseInt(input.readLine());
					cryptedMsg = new char[lineLen];
					input.read(cryptedMsg, 0, lineLen);
					msg = PeenoServer.getCipher().decryptWithPrivate(new String(cryptedMsg));
					onRecoveryKeyReceived(msg);
				} else if (inputLine.equals("MSG")) {
					ConfigManager.log("MSG RECEIVED");

					int sID = Integer.parseInt(input.readLine());
					lineLen = Integer.parseInt(input.readLine());
					cryptedMsg = new char[lineLen];
					input.read(cryptedMsg, 0, lineLen);
					//MC  a quest'ora non mi è venuto in mente di meglio di un doppio controllo su userAuthenticated
					//scusate vengo dal C :P
					if (!userAuthenticated) {
						restoreSession(sID);
					}
					//separated if because authRegistered may be updated
					if (userAuthenticated) {
						ConfigManager.log("Msg len: " + cryptedMsg.length);
						onMsgReceived(new String(cryptedMsg));
					}
				} else {
					ConfigManager.log("ERROR: Message not known say WHAT?");
					output.println("WHAT?");
					System.exit(-1);
				}
			} 
		} catch (IOException e) {
			ConfigManager.log("Error in thread communication: Exiting");
			e.printStackTrace();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			try {
				input.close();
				output.close();
				socket.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Parse recoveryKeyMsg force re-download of accepted friends and send the return msg with the key and timestamp++
	 * @param msg
	 */
	private void onRecoveryKeyReceived(String msg) {
		KeyRecoveryMessage m = parser.parseRecMessage(msg);
		if (usrManager.verify(m.getUsername(), m.getCryptedPwd(), false)) {
			friendsDownloadForced = true;
			usrManager.forceReloadFriendList(m.getUsername());
			KeyRecoveryResponseMessage retM = new KeyRecoveryResponseMessage();
			retM.setPrivKey(usrManager.loadPrivateKey(m.getUsername()));
			retM.setPubKey(new String(Base64.encode((keyManager.loadUserKey(m.getUsername())).getEncoded())));
			retM.setTimestamp(m.getTimestamp() + 1);
			output.println("REC/ACK");
			output.println(retM.toString().length());
			output.print(retM.toString());
			output.flush();
		}
		else {
			output.println("REC/FAIL");
		}
	}

	// ask usrmanager if session is valid, if it is restore session key and
	// others, else send a fail message
	// TODO ma sono necessarie due query? Mesa che è meglio cosi
	private void restoreSession(int sessionId) {
		if (usrManager.isSessionValid(sessionId)) {
			this.sessionId = sessionId;
			this.currentUser = usrManager.loadSessionUser(sessionId);
			//ConfigManager.log("Restored key is: "+ usrManager.loadSessionKey(sessionId));
			//this.sessionKey = SymCipher.stringToKey(usrManager.loadSessionKey(sessionId));
			sessionCipher= new SymCipher();
			sessionCipher.setKey(usrManager.loadSessionKey(sessionId));
			sessionCipher.setSessionId(sessionId);
			this.userAuthenticated = true;
			
			ConfigManager.log("Session " + sessionId + " restored");
		} else {
			ConfigManager.log("Request for session " + sessionId + " failed");
			sendFailMessage("Session expired");
		}
	}

	private void sendFailMessage(String errstring) {
		output.println("FAIL");
		output.println(errstring);
		
	}

	//server-side onsynack check only if timestamp returned is correct
	private void onSynAckReceived(String msg) {
		SynAckMessage sackMsg = parser.parseSynAckMessage(msg);
		if (sackMsg.getTimestampServer() != timeStamp) {
			ConfigManager.log("il client non ha ritornato un timestamp corretto meglio chiudere qua va");
			System.exit(-1);
		} else {
			ConfigManager.log("user " + currentUser.getUsername() + " is authenticated with session id " + sessionId);
			userAuthenticated = true;
		}
	}

	/**
	 * Event raised at the end of registration handshake.
	 * TODO forse dopo la registrazione meglio terminare
	 */
	private void registrationComplete() {
		// TODO tutte le cose da fare per completare la registrazione
		usrManager.saveUser(currentUser);
		ConfigManager.log("new user " + currentUser.getUsername() + " is registered!!!");
	}

	/**
	 * when a REG message is received start registration procedure
	 * - check if username, email, password exist and send REG/ACK to client
	 * 
	 */
	private void onRegReceived(String inputLine) {
		RegMessage reg = null;
		try {
			reg = (RegMessage)parser.process(inputLine, state);
		} catch (Exception e) {
			e.printStackTrace();
		}
		//TODO for debug use avoid check for aaa
		if (!usrManager.existsUsername(reg.getUsername()) && 
				!usrManager.existsEmail(reg.getEmail()) || reg.getUsername().equals("aaa"))
		{
			currentUser = new User();
			currentUser.setUsername(reg.getUsername());
			currentUser.setEmail(reg.getEmail());
			currentUser.setPassword(reg.getPassword());
			currentUser.setPubKey(reg.getPubKey());
			usrManager.storeUserPrivateKey(currentUser.getUsername(), reg.getCryptedPriv());
			sendRegAck(currentUser, reg.getTimestamp());
		} else
			sendFailRegistration();
	}
	
	/**
	 * Send REG/ACK
	 * - create new timestamp for client authentication
	 * - send reg/ack ciphered with client public key
	 * @param user
	 * @param ts
	 */
	private void sendRegAck(User user, long ts) {
		tempInteger = ts +1;
		ConfigManager.log("mando regAck");
		output.println("REG/ACK");
		String outputLine;
		AsymCipher cipher = new AsymCipher(user);
		cipher.setPublicKey(user.getPubKey());
		try {
			ConfigManager.log("" + tempInteger);
			outputLine=cipher.cryptWithPublicKey("" + tempInteger );
			output.println(outputLine.length());
			output.print(outputLine);
			output.flush();
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Send the REG/FAIL fail in registration
	 * TODO forse da specificare meglio perchè è fallita
	 * inoltre molto probabilmente al fail sarebbe meglio chiudere tanto alla nuova pressione su "Registra!" 
	 * conviene * di più creare un nuovo socket
	 */
	private void sendFailRegistration() {
		tempInteger = -1;
		currentUser = null;
		output.println("REG/FAIL");
	}

	/**
	 * Parse syn message generate new sessionkey, id and reprint synackmsg to client
	 * @param msg
	 */
	private void authSynReceived(String msg) {
		// TODO controllare se esiste un utente, se è valida la passwd
		SynMessage smsg = parser.parseSynMessage(msg);
		if (!usrManager.verify(smsg.getUsername(), smsg.getPassword(), true)) {
			ConfigManager.log("Wrong username or password, sending 403");
			output.println("403");
			return;
		}
		SynAckMessage sackMsg = new SynAckMessage();
		sackMsg.setTimestampClient(smsg.getTimeStamp());
		sessionCipher = new SymCipher();
		try {
			sessionCipher.generateKey();
		} catch (NoSuchAlgorithmException e) {
			e.printStackTrace();
		}
		currentUser = usrManager.loadByUsername(smsg.getUsername());
		sessionKey = sessionCipher.getKey(); 
		sessionId = usrManager.newSession(smsg.getUsername(), SymCipher.keyToString(sessionKey));
		sessionCipher.setSessionId(sessionId);
		sackMsg.setSessionId(sessionId);
		sackMsg.setSessionKey(new String (Base64.encode(sessionKey.getEncoded())));
		sackMsg.setTimestampClient(smsg.getTimeStamp());
		timeStamp = new Date().getTime();
		sackMsg.setTimestampServer(timeStamp);
		AsymCipher clientCipher = new AsymCipher("user");
		clientCipher.setPublicKey(currentUser.getPubKey());
		String ciphMsg="";
		try {
			ciphMsg = clientCipher.cryptWithPublicKey(sackMsg.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
		output.println("SYN/ACK");
		output.println(ciphMsg.length());
		output.print(ciphMsg);
		output.flush();
	}
	
	/**
	 * Init function for worker, get all configuration param and init socket
	 */
	private void initWorker() {
		try {
			output = new PrintWriter(socket.getOutputStream(), true);
			input = new BufferedReader(new InputStreamReader(
						socket.getInputStream()));
			parser = new RequestParser();
			keyManager = new KeyManager(
					ConfigManager.getKeysDir(),
					ConfigManager.getKeyPassword());
			sqlManager = new SQLManager(
					ConfigManager.getDbAddress() + ":"  + 
					ConfigManager.getDbPort(), 
					ConfigManager.getDbName(), 
					ConfigManager.getDbUsername(), 
					ConfigManager.getDbPassword());
			usrManager = new UserManager(sqlManager, keyManager);
			msgManager = new MessageManager(this, sqlManager);
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * TODO Parso il lowMessage, magari faccio una gestione degli errori di
	 * trasmissione (controllo il contatore del messaggio e vedo se ci sono
	 * cose strane) lo dejsonifico e lo mando al msgmanager
	 */
	public void onMsgReceived(String lmsgString) {
		ConfigManager.log("ON MESSAGE RECEIVED");
		usrManager.updateSessionTime(getUser().getId());
		String lmsgs = null;
		try {
			lmsgs = sessionCipher.decrypt(lmsgString);
		} catch (Exception e1) {
			e1.printStackTrace();
		}
		LowMessage lmsg = parser.parseMsgMessage(lmsgs);
		try {
			HighLevelMessage msg = (HighLevelMessage) Javabean2JSON
					.fromJSONObject(new JSONObject(
							new String(lmsg.getPayload())));
			msgManager.onReceivedMsg(msg);
		} catch (JSONException e) {
			e.printStackTrace();
		}
	}
	
	public void send(HighLevelMessage msg){
		/**TODO devo mettere il msg in formato json, dare un numero sequenziale al messaggio
		 * (problema chi lo tiene questo numero?)
		 * cifrare il lowmessage
		 * spedirlo come si deve
		 * MSG
		 * session_id
		 * msg_length
		 * msg 
		 */
		
		ConfigManager.log("SENDING TO CLIENT");
		LowMessage lmsg = new LowMessage();
		String ciphmsg = "";
		lmsg.setMsgId(msgCount++);
		lmsg.setPayload(Javabean2JSON.toJSONObject(msg).toString());
		ConfigManager.log("MSG TO SEND IS: " + lmsg.toString());
		output.println("MSG");
		output.println(sessionId);
		try {
			ciphmsg = sessionCipher.crypt(lmsg.toString());
		} catch (Exception e) {
			e.printStackTrace();
		}
		output.println(ciphmsg.length());
		int c=0;
		while (c<ciphmsg.length()) {
			output.write(ciphmsg.charAt(c));
			c++;
		}
		output.flush();
		ConfigManager.log("SENT TO CLIENT: " + ciphmsg.length() + " "  + ciphmsg.toString());
	}

	public UserManager getUserManager() {
		return usrManager;
	}

	public User getUser() {
		return currentUser;
	}

	public void logoutUser() {
		logged = false;
	}
}
